﻿using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Windows;

namespace HandyControl.Interactivity;

public abstract class AttachableCollection<T> : FreezableCollection<T>, IAttachedObject
    where T : DependencyObject, IAttachedObject
{
    private DependencyObject _associatedObject;
    private Collection<T> _snapshot;

    internal AttachableCollection()
    {
        INotifyCollectionChanged changed = this;
        changed.CollectionChanged += OnCollectionChanged;
        _snapshot = new Collection<T>();
    }

    public DependencyObject AssociatedObject
    {
        get
        {
            ReadPreamble();
            return _associatedObject;
        }
    }

    DependencyObject IAttachedObject.AssociatedObject => AssociatedObject;

    public void Attach(DependencyObject dependencyObject)
    {
        if (Equals(dependencyObject, AssociatedObject))
            return;
        if (AssociatedObject != null)
            throw new InvalidOperationException();
        if (Interaction.ShouldRunInDesignMode || !(bool) GetValue(DesignerProperties.IsInDesignModeProperty))
        {
            WritePreamble();
            _associatedObject = dependencyObject;
            WritePostscript();
        }
        OnAttached();
    }

    public void Detach()
    {
        OnDetaching();
        WritePreamble();
        _associatedObject = null;
        WritePostscript();
    }

    protected abstract void OnAttached();

    protected abstract void OnDetaching();

    internal abstract void ItemAdded(T item);

    internal abstract void ItemRemoved(T item);

    private void VerifyAdd(T item)
    {
        if (item == null) throw new ArgumentNullException(nameof(item));
        if (_snapshot.Contains(item))
            throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                ExceptionStringTable.DuplicateItemInCollectionExceptionMessage, new object[]
                {
                    typeof(T).Name,
                    GetType().Name
                }));
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                var enumerator1 = e.NewItems.GetEnumerator();
                try
                {
                    while (enumerator1.MoveNext())
                    {
                        var current = (T) enumerator1.Current;
                        try
                        {
                            VerifyAdd(current);
                            ItemAdded(current);
                        }
                        finally
                        {
                            _snapshot.Insert(IndexOf(current), current);
                        }
                    }
                    break;
                }
                finally
                {
                    if (enumerator1 is IDisposable disposable)
                        disposable.Dispose();
                }
            case NotifyCollectionChangedAction.Remove:
                var enumerator2 = e.OldItems.GetEnumerator();
                try
                {
                    while (enumerator2.MoveNext())
                    {
                        var current = (T) enumerator2.Current;
                        ItemRemoved(current);
                        _snapshot.Remove(current);
                    }
                    break;
                }
                finally
                {
                    if (enumerator2 is IDisposable disposable)
                        disposable.Dispose();
                }
            case NotifyCollectionChangedAction.Replace:
                foreach (T oldItem in e.OldItems)
                {
                    ItemRemoved(oldItem);
                    _snapshot.Remove(oldItem);
                }
                var enumerator3 = e.NewItems.GetEnumerator();
                try
                {
                    while (enumerator3.MoveNext())
                    {
                        var current = (T) enumerator3.Current;
                        try
                        {
                            VerifyAdd(current);
                            ItemAdded(current);
                        }
                        finally
                        {
                            _snapshot.Insert(IndexOf(current), current);
                        }
                    }
                    break;
                }
                finally
                {
                    if (enumerator3 is IDisposable disposable)
                        disposable.Dispose();
                }
            case NotifyCollectionChangedAction.Reset:
                foreach (var obj in _snapshot)
                    ItemRemoved(obj);
                _snapshot = new Collection<T>();
                using (var enumerator4 = GetEnumerator())
                {
                    while (enumerator4.MoveNext())
                    {
                        var current = enumerator4.Current;
                        VerifyAdd(current);
                        ItemAdded(current);
                    }
                    break;
                }
        }
    }
}
